package bmechat.client;

import filetransfer.FileClient;
import filetransfer.FileServer;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractListModel;
import javax.swing.event.ChangeListener;

/**
 *
 * @author david
 * The Client class implements all functionality to connect to a server running the
 * Server application.
 */
public class Client extends AbstractListModel implements Runnable {

    private String name;
    private String server;
    private int serverPort;
    private Socket socketToServer;
    private ServerMessageReader serverMessageReader;
    private PrintWriter out;
    private boolean connected = false;
    private boolean admin = false;
    private int numberOfClientsOnServer = 0;
    private List<String> listOfClientsOnServer;
    private List<String> chatLog;
    private List<ChangeListener> chatLogListeners;
    private ExecutorService pool;
    private File file;

    /**
     * Constructor for the client class. Initializes the connection info using the given parameters.
     * @param name The client's preferred nickname.
     * @param server The hostname the server is running on.
     * @param serverPort The port is server is listening on.
     */
    public Client(String name, String server, int serverPort) {
        this.name = name;
        this.server = server;
        this.serverPort = serverPort;
        this.chatLog = new ArrayList<String>();
        this.chatLogListeners = new ArrayList<ChangeListener>();
    }

    /**
     * Connect to the server, using supplied hostname and port.
     * @param server The hostname the server is running on.
     * @param port The port is server is listening on.
     * @throws UnknownHostException
     * @throws IOException
     */
    private void connectToServer(String server, int port) throws UnknownHostException, IOException {
        socketToServer = new Socket(server, port);
        pool = Executors.newFixedThreadPool(5);
        serverMessageReader = new ServerMessageReader(socketToServer.getInputStream(), this);
        pool.execute(serverMessageReader);
        out = new PrintWriter(socketToServer.getOutputStream(), true);
        connected = true;
    }

    /**
     * Connect to the server, using previously entered hostname and port.
     * @throws UnknownHostException
     * @throws IOException
     */
    public void connectToServer() throws UnknownHostException, IOException {
        connectToServer(server, serverPort);
    }

    /**
     * Start the client.
     * - Connects to the server.
     * - Sends connection settings to the server.
     * - Starts accepting user input.
     */
    public void run() {
        try {
            connectToServer();
            sendSettingsToServer();
        } catch (UnknownHostException e) {
            addToChatLog("Unknown Host! Can't connect!");
        } catch (IOException e) {
            addToChatLog("Unknown IO error while connection to server " + server);
        }
        acceptUserInput();
    }

    /**
     * Accept user input until shutdown.
     */
    public void acceptUserInput() {
        //JOINED, ACCEPTING USER INPUT
        Scanner sc = new Scanner(System.in);
        while (true) {
            String keyboardInput = sc.nextLine();
            handleKeyboardInput(keyboardInput);
        }
    }

    /**
     * Send commands to the server before the client is added to the channel.
     * #join adds the client to the channel.
     * @throws IOException
     */
    public void sendSettingsToServer() throws IOException {
        //SETTINGS BEFORE JOINING
        sendMessageToServer("#name = " + name);
        sendMessageToServer("#list");
        sendMessageToServer("#join");
    }

    /**
     * Process input from the user.
     * Commands start with a '#'.
     * If a command means nothing special to the client, forward the command to the server.
     * @param input a line of user input.
     */
    private void handleKeyboardInput(String input) {
        try {
            if (input.equals("#disconnect")) {
                disconnect(true);
            } else if (input.startsWith("#server")) {
                int equalsIndex = input.indexOf('=');
                if (equalsIndex == -1) {
                    throw new IllegalArgumentException("no '=' was found. Use \"#server = serveraddress:serverport\" to change server!");
                }
                String newServer = input.substring(equalsIndex + 1).trim();
                setServer(newServer);
            } else if (input.equals("#connect")) {
                connectToServer();
                sendSettingsToServer();
                acceptUserInput();
            } else if (input.equals("#quit")) {
                try {
                    disconnect(true);
                    addToChatLog("Quitting");
                } catch (IOException e) {
                }
                System.exit(0);
            } else {
                sendMessageToServer(input);
            }
        } catch (IOException ex) {
            addToChatLog("Error while processing user input: " + ex.getMessage());
        } catch (IllegalArgumentException ex) {
            addToChatLog("Syntax Error: " + ex.getMessage());
        }

    }

    /**
     * Disconnect from the server.
     * @param clientInitiative TRUE if the client started the disconnecting sequence, FALSE when the server started disconnecting.
     * @throws IOException
     */
    public void disconnect(boolean clientInitiative) throws IOException {
        try {
            if (clientInitiative) {
                sendMessageToServer("#disconnect");
            }
            serverMessageReader.stop();
            addToChatLog("Closing connection to " + socketToServer.getInetAddress().getHostName() + " / " + socketToServer.getInetAddress().getHostAddress());
            socketToServer.close();
            connected = false;
        } catch (IOException ex) {
            throw new IOException("Error while disconnecting");
        }
    }

    /**
     * Send a message to the server.
     * @param message The chatmessage or command to send to the server.
     * @throws IOException
     */
    private void sendMessageToServer(String message) throws IOException {
        if (connected) {
            out.println(message);
        } else {
            throw new IOException("Not connected to server");
        }
    }

    /**
     * Set new connection settings (hostname and port).
     * @param newServer Connection settings (hostname:port)
     */
    private void setServer(String newServer) {
        if (newServer.contains(":")) {
            int colonIndex = newServer.indexOf(':');
            server = newServer.substring(0, colonIndex);
            serverPort = Integer.parseInt(newServer.substring(colonIndex + 1));
        } else {
            server = newServer;
        }
        addToChatLog("Connection settings: " + server + ":" + serverPort);
    }

    public static void main(String args[]) {
        ExecutorService pool = Executors.newFixedThreadPool(5);
        pool.execute(new Client(args[0], args[1], Integer.parseInt(args[2])));
    }

    /**
     * Get the number of clients on the server.
     * @return The number of clients on the server.
     */
    public int getSize() {
        return numberOfClientsOnServer;
    }

    /**
     * Returns the name of a client on the server
     * @param index The index of the client in the server's clientlist
     * @return
     */
    public Object getElementAt(int index) {
        return listOfClientsOnServer.get(index);
    }

    /**
     * Requests a new clientlist from the server.
     */
    public void requestNewClientList() {
        out.println("#list");
    }

    /**
     * Returns a log of all messages sent.
     * @return a list of strings, one for each message.
     */
    public List<String> getChatLog() {
        return chatLog;
    }

    /**
     * Add a message to the chatlog.
     * @param message The message that needs to be added to the chatlog.
     */
    public void addToChatLog(String message) {
        chatLog.add(message);
        fireChatLogChanged();
    }

    /**
     * Tries to send a message to the server. If this fails, the exception will be shown in the chatwindow.
     * @param message
     */
    public void sendMessage(String message) {
        try {
            sendMessageToServer(message);
        } catch (Exception e) {
            chatLog.add(e.getMessage());
        }
    }

    /**
     * Returns a local list of clients on the server.
     * @return A list of clients on the server.
     */
    public List<String> getListOfClientsOnServer() {
        return listOfClientsOnServer;
    }

    /**
     * Update the local list of clients on the server.
     * @param listOfClientsOnServer A recent list of clients on the server.
     */
    public void setListOfClientsOnServer(List<String> listOfClientsOnServer) {
        this.listOfClientsOnServer = listOfClientsOnServer;
        fireContentsChanged(this, 0, listOfClientsOnServer.size());
    }

    /**
     * Gets the client's name.
     * @return The client's name.
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the client's nickname.
     * @param name The client's new nickname.
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Get the locally cached number of clients on the server.
     * @return The number of clients on the server.
     */
    public int getNumberOfClientsOnServer() {
        return numberOfClientsOnServer;
    }

    /**
     * Update the number of clients on the server.
     * @param numberOfClientsOnServer The new number of clients on the server.
     */
    public void setNumberOfClientsOnServer(int numberOfClientsOnServer) {
        this.numberOfClientsOnServer = numberOfClientsOnServer;
    }

    /**
     * Add a listener for updates to the chatlog.
     * @param l The listener to add.
     */
    public void addChatLogListener(ChangeListener l) {
        chatLogListeners.add(l);

    }

    public String getServer() {
        return server;
    }

    public int getServerPort() {
        return serverPort;
    }

    public boolean isAdmin() {
        return admin;
    }

    public void setAdmin(boolean admin) {
        this.admin = admin;
    }

    /**
     * Tell all listeners that the chatlog was updated.
     */
    private void fireChatLogChanged() {
        for (ChangeListener l : chatLogListeners) {
            l.stateChanged(null);
        }
    }

    public String getIName() {
        try {
            java.net.URL URL = new java.net.URL("http://www.whatismyip.org/");
            java.net.HttpURLConnection Conn = (HttpURLConnection) URL.openConnection();
            java.io.InputStream InStream = Conn.getInputStream();
            java.io.InputStreamReader Isr = new java.io.InputStreamReader(InStream);
            java.io.BufferedReader Br = new java.io.BufferedReader(Isr);
            return Br.readLine();
        } catch (Exception ex) {
            return "unknown";
        }
    }

    public void receiveFile(int filesize, String sender, String newFileName) {
        String[] path = new String[3];
        path[0] = newFileName;
        path[1] = Integer.toString(filesize);
        path[2] = sender;

        this.addToChatLog("Receiving file...");
        pool.execute(new FileClient(path, this));
    }

    public void sendFile() {
        String[] path = new String[1];
        path[0] = this.getFile().getAbsolutePath();

        this.addToChatLog("Sending file...");
        pool.execute(new FileServer(path, this));
    }

    public void setFile(File file) {
        this.file = file;
    }

    public File getFile() {
        return this.file;
    }
}
